接下来的一系列文件将会从源码角度来分析Small的架构以及插件化原理及其实现。
整体架构
Small里面比较核心的类有下面三个:
- Small:接口类,提供用户能使用的各类接口
- Bundle:代表插件类,保存了插件的全部信息
- BundleLauncher:插件加载类,根据加载的不同插件类型,有多个子类,如下图:
初始化
先来看一下宿主 App 中的初始化部分,主要在 Application
和 LaunchActivity
中进行。我们把在 Application
处理的称为第一阶段,在 LaunchActivity
中进行的称为第二阶段和第三阶段。
第一阶段:预处理
1 | public class Application extends android.app.Application { |
在 Application
构造函数中调用了 Small.preSetUp(this)
来进行一些设置的工作:
1 | public static void preSetUp(Application context) { |
首先注册了一些默认的 BundleLauncher
,保存在 sBundleLaunchers
静态变量中。然后调用 Bundle.onCreateLaunchers(context)
来调用 BundleLauncher.onCreate()
方法。
在几个 BundleLauncher
的子类中,ApkBundleLauncher
重新实现了 onCreate()
方法。
1 |
|
InstrumentationWrapper
继承自 Instrumentation
并覆盖了下面几个方法:
1 | execStartActivity() |
为什么说替换 Instrumentation
对象是重头戏呢?这里我们先了解一些这个类。
先看一下官方文档对这个类的解释,该类跟踪 Application 及 Activity 的整个生命周期,它的一些方法在 Application 及 Activity 所有生命周期函数的调用中,都会先调用这些方法,因此,得到了这个对象,我们就可以进入并跟踪 Application 和 Activity 的生命周期流程。
Small 想要做到动态注册 Activity
,首先在宿主 Manifest 中注册一个命名特殊的占坑 Activity
来欺骗 startActivityForResult
以获得生命周期,再欺骗 performLaunchActivity
来获得插件 Activity
实例,又为了处理之间的信息传递,因此有了后面的 ActivityThreadHandlerCallback
。
我们可以在 small/src/main/AndroidManifest.xml 中找到这些占坑位的 Activity
: A、A1、A2….A33等。
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
所作的这一切都是为了实现动态注册 Activity
,如果你把插件里面的 Activity
都在宿主的 AndroidManifest.xml 文件里面都申明一下,那么上面的这些 Hook 的工作就可以省去了。
这也就是 Small 插件化的基本原理,该原理部分后面会有博客详细介绍。
第二阶段:加载插件
一些配置工作
在 LaunchActivity
的 onStart()
方法中调用了 Small.setUp()
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class LaunchActivity extends Activity {
protected void onStart() {
...
Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
public void onComplete() {
long tEnd = System.nanoTime();
se.putLong("setUpFinish", tEnd).apply();
long offset = tEnd - tStart;
if (offset < MIN_INTRO_DISPLAY_TIME) {
// 这个延迟仅为了让 "Small Logo" 显示足够的时间, 实际应用中不需要
getWindow().getDecorView().postDelayed(new Runnable() {
public void run() {
// 启动main插件
Small.openUri("main", LaunchActivity.this);
finish();
}
}, (MIN_INTRO_DISPLAY_TIME - offset) / 1000000);
} else {
Small.openUri("main", LaunchActivity.this);
finish();
}
}
});
}
在 Small.setUp()
方法内部主要调用了 Bundle.loadLaunchableBundles(listener)
。
1 | protected static void loadLaunchableBundles(Small.OnCompleteListener listener) { |
由于我们注册了了 Small.OnCompleteListener
,这里会开启一个线程来调用 loadBundles()
方法。
1 | private static void loadBundles(Context context) { |
在 loadBundles()
方法中首先会解析 bundle.json 数据,这个数据可能会保存在三个地方,它们的读取是有优先级的,SharedPreferences缓存>App DATA File>Assets。
然后调用 setupLaunchers()
设置前面在 preSetup()
方法中注册的几个 BundleLauncher
。
1 | protected static void setupLaunchers(Context context) { |
我们分别看一下这几个 BundleLauncher
的 setUp()
方法都做了什么工作:
- ActivityLauncher.setUp()
这里是将在宿主App里面注册的 Activity
添加到 sActivityClasses
中去,这里包括了 app、app+stub、small下面 AndroidMenifest.xml里面注册的 Activity
,当然就包括了前面说的占坑位的几个 Activity
。
1 |
|
- ApkBundleLauncher.setUp()
这里是对通过动态代理对所有经过 TaskStackBuilder
创建的 PendingIntent 进行 Hook,调用 wrapIntent
用占坑 Activity
来代替真正的 Activity
。
另外还有个方法 Small.wrapIntent(Intent)
,不是通过TaskStackBuilder
创建的 PendingIntent
需要调用这个方法来进行处理。
1 |
|
- WebBundleLauncher.setUp()
看到注释这样解释:在android 7.0以后的版本中,当第一次创建WebView的时候,它会用WebView的Assets路径替换掉原Application Assets路径,这里就提前在这里先创建一个WebView来避免这个事件的发生。
1 |
|
在 setupLaunchers(context)
方法执行完以后,就会调用 loadBundles(manifest.bundles)
方法来加载插件。
加载插件
先来看一下 Bundle.loadBundles(List<Bundle> bundles)
方法,这个方法的主要工作就是在注册的所有 BundleLauncher
中为 bundles
列表中的所有 Bundle
找到适合它们的 BundleLauncher
,
1 | private static void loadBundles(List<Bundle> bundles) { |
prepareForLaunch()
我们先来看一下 Bundle.prepareForLaunch()
方法,这里是要在 sBundleLaunchers
中为当前的 Bundle
找到一个合适的 BundleLauncher
并赋值给 mApplicableLauncher
,并开始解析插件。
1 | protected void prepareForLaunch() { |
这里又分别调用了 sBundleLaunchers
中各个 BundleLauncher
的 resolveBundle()
方法。
1 | public boolean resolveBundle(Bundle bundle) { |
各个 BundleLauncher
都分别重新实现了 preloadBundle(bundle)
和 loadBundle(bundle)
方法,我们分别来看一下。
ActivityLauncher
1 |
|
这里在 mPackageName
为 main 时才会返回true,ActivityLauncher
是用来启动宿主 Activity
的,它并没有实现 loadBundle
方法,因此就算 preloadBundle()
方法返回true,它也不会有任何处理的。
SoBundleLauncher.preloadBundle()
因为 ApkBundleLauncher
没有覆盖 preloadBundle()
方法,那么就到了它的父类 SoBundleLauncher.preloadBundle()
方法。
1 |
|
插件的解析由 BundleParser
类来完成,不再详述,可以自己分析源码。
ApkBundleLauncher.loadBundle()
为插件创建 LoadedApk
对象,加载dex文件以及lib库,提取Activity并放入sLoadedActivities列表,收集intentFilter并存入sLoadedIntentFilters列表。
1 |
|
AssetBundleLauncher.loadBundle()
WebBundleLauncher
的 loadBundle()
方法也由它的父类 AssetBundleLauncher
来处理,由于 AssetBundleLauncher
是继承自 SoBundleLauncher
,因此 preloadBundle()
也由 SoBundleLauncher
处理。
这个方法主要是将插件文件路径转化为index文件路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
// 获取插件路径
File unzipDir = new File(getBasePath(), packageName);
// 获取indexfile文件,WebBundleLauncher就是在unzipDir后面加上/index.html
File indexFile = new File(unzipDir, getIndexFileName());
// Prepare index url
String uri = indexFile.toURI().toString();
if (bundle.getQuery() != null) {
uri += "?" + bundle.getQuery();
}
URL url;
try {
url = new URL(uri);
} catch (MalformedURLException e) {
Log.e(TAG, "Failed to parse url " + uri + " for bundle " + packageName);
return;
}
String scheme = url.getProtocol();
if (!scheme.equals("http") &&
!scheme.equals("https") &&
!scheme.equals("file")) {
Log.e(TAG, "Unsupported scheme " + scheme + " for bundle " + packageName);
return;
}
bundle.setURL(url);
}
BundleLauncher.postSetUp()
这里也会调用 BundleLauncher
各个子类的 BundleLauncher
方法。
但是仅有 ApkBundleLauncher
覆盖了基类的空实现。
1 |
|
至此,插件的初始化部分介绍完毕。